重要度別にGuardDutyをSlackに通知してみた

重要度別にGuardDutyをSlackに通知してみた

Clock Icon2022.04.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。たかやまです。

こちらのブログのようにGuardDutyはSlackに通知することができますが、今回はこのGuardDutyの通知を重要度別にチャンネルへ飛ばす実装をしたいと思います。

構成

構成

Slackイメージ図

設定方法

構築はCDKを利用しています。リポジトリはこちらです。

SNS

2つのSlackチャンネルに連携するために、スタンダードのSNS トピックを2つ用意します。

  • Severity Highを通知するトピック
  • Severity Middleを通知するトピック

サブスクリプションはこのあとのチャットボットとの紐付けで設定されるため、この段階では設定不要です。

cdk(クリックで展開)

import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as sns from "aws-cdk-lib/aws-sns";

export interface props extends StackProps {
  projectName: String;
  envName: String;
}

export class SnsStack extends Stack {
  public readonly snsTopicForHigh: sns.ITopic;
  public readonly snsTopicForMiddleLow: sns.ITopic;

  constructor(scope: Construct, id: string, props: props) {
    super(scope, id, props);

    /**
      *  Create a SNS for the eventbridge
      */
    this.snsTopicForHigh = new sns.Topic(this, `${props.projectName}-${props.envName}-for-guardduty-high`, {
      topicName: `${props.projectName}-${props.envName}-for-guardduty-high`,
      displayName: `${props.projectName}-${props.envName}-for-guardduty-high`,
    });

    this.snsTopicForMiddleLow = new sns.Topic(this, `${props.projectName}-${props.envName}-for-guardduty-middle-low`, {
      topicName: `${props.projectName}-${props.envName}-for-guardduty-middle`,
      displayName: `${props.projectName}-${props.envName}-for-guardduty-middle`,
    });
  }i
}

Lambda

重要度別にGuardDutyイベントを振り分ける処理をLambdaで行います。
単に重要度だけで分けるのであれば、EventBridgeのフィルター機能が利用可能です。

今回は後述するイベントのカスタマイズもしたいため、Lambdaを使っていきます。
Lambdaはseverityの値で通知するSNSトピックを振り分ける処理を行っています。

重要度レベル Severity
[High] (高) 8.9 - 7.0
[Medium] (中) 6.9 - 4.0
[Low] (低) 3.9 - 1.0

import json
import os
import boto3

SNS_TOPIC_ARN_HIGH = os.environ["SNS_TOPIC_ARN_HIGH"]
SNS_TOPIC_ARN_MIDDLE_LOW = os.environ["SNS_TOPIC_ARN_MIDDLE_LOW"]
client = boto3.client("sns")


def lambda_handler(event, context):

    message = json.dumps(event)
    print(type(event["detail"]["severity"]))
    if event["detail"]["severity"] >= 7:
        client.publish(TopicArn=SNS_TOPIC_ARN_HIGH, Message=message)
    else:
        client.publish(TopicArn=SNS_TOPIC_ARN_MIDDLE_LOW, Message=message)

cdk(クリックで展開)

import * as path from "path";

import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as sns from "aws-cdk-lib/aws-sns";

export interface props extends StackProps {
  projectName: String;
  envName: String;
  snsTopicForHigh: sns.ITopic;
  snsTopicForMiddleLow: sns.ITopic;
}

export class LambdaStack extends Stack {
  public readonly lambdaFunction: lambda.IFunction;

  constructor(scope: Construct, id: string, props: props) {
    super(scope, id, props);

    /**
      * Create a Role
      */
    const role = new iam.Role(this, `${props.projectName}-${props.envName}-role`, {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
    });
    role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSNSFullAccess"));
    role.addManagedPolicy(
      iam.ManagedPolicy.fromManagedPolicyArn(
        this,
        `${props.projectName}-${props.envName}-lambdabasic`,
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      )
    );

    /**
      *  Create a Lambda
      */
    this.lambdaFunction = new lambda.Function(this, `${props.projectName}-${props.envName}-lambda`, {
      functionName: `${props.projectName}-${props.envName}-lambda`,
      runtime: lambda.Runtime.PYTHON_3_9,
      handler: "lambda_function.lambda_handler",
      environment: {
        SNS_TOPIC_ARN_HIGH: props.snsTopicForHigh.topicArn,
        SNS_TOPIC_ARN_MIDDLE_LOW: props.snsTopicForMiddleLow.topicArn,
      },
      code: lambda.Code.fromAsset(path.join(__dirname, "./assets")),
      role: role,
    });
  }
}

EventBridge

GuardDutyが発生させたイベントはEventBridge経由でLambdaに連携していきます。
イベントのフィルターはLambdaで実施するので、EventBridgeはシンプルなイベントパターン設定となります。

ターゲットは作成したLambdaを指定します。

cdk(クリックで展開)

import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as lambda from "aws-cdk-lib/aws-lambda";

export interface props extends StackProps {
  projectName: String;
  envName: String;
  lambdaFunction: lambda.IFunction;
}

export class EventBridgeStack extends Stack {
  constructor(scope: Construct, id: string, props: props) {
    super(scope, id, props);

    /**
      *  Create a EventBridge
      */
    const rule = new events.Rule(this, `${props.projectName}-${props.envName}-events-rule`, {
      ruleName: `${props.projectName}-${props.envName}-events-rule`,
      eventPattern: {
        source: ["aws.guardduty"],
        detailType: ["GuardDuty Finding"],
      },
      targets: [new targets.LambdaFunction(props.lambdaFunction)],
    });
  }
}

ChatBot

初期設定

Slackの設定がなければ、手動で設定していきます。

slackを利用している場合リダイレクトされるので許可します。

ワークスペースが作成されます。
Cfn/CDKで作成する場合、ここで表示されるワークスペースIDを利用する形になります。

あとは、チャンネルIDと先程作成したSNSトピックを紐付けます。

cdk(クリックで展開)

import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as chatbot from "aws-cdk-lib/aws-chatbot";
import * as sns from "aws-cdk-lib/aws-sns";

export interface props extends StackProps {
  projectName: String;
  envName: String;
  snsTopicForHigh: sns.ITopic;
  snsTopicForMiddleLow: sns.ITopic;
  slackWorkspaceId: string;
  slackChannelId1: string;
  slackChannelId2: string;
}

export class ChatBotStack extends Stack {
  constructor(scope: Construct, id: string, props: props) {
    super(scope, id, props);

    /**
     *  Create a Chatbot
     */
    const slackChannel1 = new chatbot.SlackChannelConfiguration(this, `${props.projectName}-${props.envName}-guardduty-high`, {
      slackChannelConfigurationName: "guardduty-high",
      slackWorkspaceId: props.slackWorkspaceId,
      slackChannelId: props.slackChannelId1,
      notificationTopics: [props.snsTopicForHigh],
    });

    const slackChannel2 = new chatbot.SlackChannelConfiguration(this, `${props.projectName}-${props.envName}-guardduty-middle-low`, {
      slackChannelConfigurationName: "guardduty-middle-low",
      slackWorkspaceId: props.slackWorkspaceId,
      slackChannelId: props.slackChannelId2,
      notificationTopics: [props.snsTopicForMiddleLow],
    });
  }
}

テスト

設定が完了したら、GuardDutyの結果サンプルの生成を使って通知が行くか試します。
設定が問題なければ、最初のイメージ図のようなかたちで通知がいくかと思います。

通知のカスタマイズ

Lambdaを利用した理由は、通知のカスタマイズをしたいためです。

今回はAWSが定義している重要度とは別に特定の通知を重要度HighのSlackに通知したい場合を想定してカスタマイズしてみます。

例として、重要度Mediumの通知Exfiltration:S3/ObjectRead.Unusualを重要度Highのslackチャンネルに通知してみたいと思います。

サンプルコードはこちらです。


import json
import os

import boto3

SNS_TOPIC_ARN_HIGH = os.environ["SNS_TOPIC_ARN_HIGH"]
SNS_TOPIC_ARN_MIDDLE_LOW = os.environ["SNS_TOPIC_ARN_MIDDLE_LOW"]
client = boto3.client("sns")


def lambda_handler(event, context):

    high_list = ["Exfiltration:S3/ObjectRead.Unusual"]
    message = json.dumps(event)
    if event["detail"]["severity"] >= 7 or event["detail"]["type"] in high_list:
        client.publish(TopicArn=SNS_TOPIC_ARN_HIGH, Message=message)
    else:
        client.publish(TopicArn=SNS_TOPIC_ARN_MIDDLE_LOW, Message=message)

再度結果サンプルの生成を行うと、重要度Highのチャンネルに無事通知されていることを確認できました。

※severityの表記やアイコン色も変えたい場合は、コード内でseverityの値を書き換える

まとめ

重要度別にSlack通知を実装してみました。

Lambdaを使うことで重要度をカスタマイズできるので、独自の基準で重要度を管理したい場合にはぜひLambdaをご活用ください。

以上、たかやまでした。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.